// instrument.go - 一个单文件的 Go 函数调用追踪插桩工具。
// 它通过修改 Go 源代码的抽象语法树 (AST) 来实现，
// 在函数入口处自动插入 `defer Trace()()` 语句。
package main

import (
	"bytes"         // 用于高效处理字节序列，特别是格式化代码输出
	"flag"          // 用于处理命令行参数（如 -w）
	"fmt"           // 用于格式化输入输出，包括打印追踪信息和错误信息
	"go/ast"        // Go 语言抽象语法树相关类型和函数
	"go/format"     // 用于格式化 Go 源代码
	"go/parser"     // 用于将 Go 源代码解析为 AST
	"go/token"      // Go 语言词法单元和文件集（管理源码位置）
	"io/fs"         // Go 1.16+ 文件系统接口，用于 WalkDir 和文件权限
	"os"            // 操作系统功能，如文件操作、命令行参数、环境变量
	"path/filepath" // 用于处理和操作文件路径，跨平台兼容
	"runtime"       // 提供与 Go 运行时交互的功能，如获取调用栈、goroutine ID
	"strconv"       // 字符串和其他基本数据类型之间的转换
	"strings"       // 字符串操作函数
	"sync"          // 提供基本的同步原语，如 sync.Map
	// "golang.org/x/tools/go/ast/astutil" 在 Go 1.20+ 已被集成到 go/ast 中
	// 如果使用 < Go 1.20，请取消下一行的注释并相应调整代码
	// "golang.org/x/tools/go/ast/astutil" // AST 操作的辅助函数
	// 注意：Go 1.20+ astutil 的功能移到了 go/ast 包，下面的代码已适配
)

// ============================================================================
// 1. 追踪逻辑 (实现具体的追踪行为)
// ============================================================================

var (
	// goroutineSpace 用于在 runtime.Stack 输出中识别 goroutine ID 的前缀字节序列。
	goroutineSpace = []byte("goroutine ")
	// goroutineIndent 使用 sync.Map 存储每个 goroutine 的当前调用栈深度（用于缩进）。
	// sync.Map 适用于读多写少且键值对数量不定的场景，适合存储 goroutine 状态。
	// key: goroutine ID (uint64), value: indent level (int64)
	goroutineIndent sync.Map
)

// 定义 ANSI 转义序列颜色常量, 用于在支持的终端中为输出着色，提高可读性。
const (
	colorReset   = "\033[0m"  // 重置所有颜色和样式
	colorGreen   = "\033[32m" // 绿色，用于 Goroutine ID
	colorBlue    = "\033[34m" // 蓝色，用于函数名
	colorMagenta = "\033[35m" // 品红色，用于行号
	// traceFuncName 是我们注入和识别的追踪函数的名字。
	// 在单文件工具中硬编码此名称简化了 AST 检查逻辑。
	traceFuncName = "Trace"
)

// curGoroutineID 获取当前执行的 goroutine 的 ID。
// 注意：此方法依赖 runtime.Stack 输出的特定格式，这种格式在不同 Go 版本间可能变化，
// 并且获取堆栈信息有性能开销。它主要用于调试和演示目的。
// 在高并发或生产环境中，可能需要更稳定或性能更好的 goroutine 识别机制。
func curGoroutineID() uint64 {
	var buf [64]byte                  // 在栈上分配一个小缓冲区，避免堆分配
	n := runtime.Stack(buf[:], false) // 获取当前 goroutine 的堆栈跟踪信息

	// 从堆栈信息中提取 Goroutine ID
	idField := bytes.TrimPrefix(buf[:n], goroutineSpace) // 移除 "goroutine " 前缀
	i := bytes.IndexByte(idField, ' ')                   // 查找 ID 后面的第一个空格
	if i < 0 {
		// 如果堆栈格式不符合预期（例如信息不完整），打印警告并返回 0
		fmt.Fprintf(os.Stderr, "警告: 获取 goroutine ID 时无法解析堆栈: %q\n", buf[:n])
		return 0 // 返回 0 表示获取失败
	}

	// 解析提取出的 ID 字符串为 uint64
	id, err := strconv.ParseUint(string(idField[:i]), 10, 64)
	if err != nil {
		// 如果解析失败，打印警告并返回 0
		fmt.Fprintf(os.Stderr, "警告: 解析 goroutine ID '%s' 失败: %v\n", string(idField[:i]), err)
		return 0 // 返回 0 表示解析失败
	}
	return id
}

// printTrace 格式化并打印单条追踪日志（函数进入或退出）。
// id: Goroutine ID
// name: 函数名称
// line: 调用追踪函数的源代码行号
// arrow: "->" 表示进入, "<-" 表示退出
// indent: 缩进级别，用于可视化调用层级
func printTrace(id uint64, name string, line int, arrow string, indent int) {
	indents := strings.Repeat("  ", indent) // 使用两个空格作为一级缩进
	// 使用 ANSI 颜色代码格式化输出到标准输出
	fmt.Printf("%sgid[%05d]%s %s%s %s%s%s:%s%d%s\n",
		colorGreen, id, colorReset, // Goroutine ID (绿色)
		indents, arrow, // 缩进和箭头
		colorBlue, name, colorReset, // 函数名 (蓝色)
		colorMagenta, line, colorReset) // 行号 (品红色)
}

// Trace 是实际被注入到目标 Go 代码中的追踪函数。
// 当被插桩的函数开始执行时，其开头的 `defer Trace()()` 会立即调用此 Trace 函数。
// Trace 函数打印函数进入信息，然后返回一个闭包函数。
// 这个闭包函数被 defer 语句注册，将在被插桩的函数执行完毕（return 或 panic）之前被调用，用于打印函数退出信息。
func Trace() func() {
	// 获取调用 Trace()() 的位置信息（即被插桩函数的入口点）
	// 参数 1 表示跳过 Trace 函数本身的调用栈帧
	pc, _, line, ok := runtime.Caller(1)
	if !ok {
		// 极少情况会失败，例如堆栈信息损坏
		fmt.Fprintf(os.Stderr, "警告: runtime.Caller(1) 失败，无法获取调用者信息\n")
		return func() {} // 返回一个空操作函数，避免 panic
	}

	// 通过程序计数器 (pc) 获取函数本身的元信息
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		fmt.Fprintf(os.Stderr, "警告: runtime.FuncForPC(%v) 失败，无法获取函数信息\n", pc)
		return func() {} // 返回空操作
	}
	name := fn.Name()       // 获取函数名 (格式通常是 "package.Receiver.Method" 或 "package.Function")
	gid := curGoroutineID() // 获取当前 goroutine ID

	// --- 函数进入逻辑 ---
	// 原子地加载或存储当前 goroutine 的缩进级别
	// LoadOrStore: 如果 gid 不存在，则存储 0 并返回 (0, false)；如果存在，加载并返回 (现有值, true)。
	rawIndent, _ := goroutineIndent.LoadOrStore(gid, int64(0))
	indent := rawIndent.(int64) // 类型断言，因为我们存的是 int64

	// 打印函数进入的追踪信息，使用当前的缩进级别
	printTrace(gid, name, line, "->", int(indent))

	// 增加当前 goroutine 的缩进级别，为后续（嵌套）调用做准备
	// 使用 Store 覆盖旧值，对于缩进跟踪足够简单且安全
	goroutineIndent.Store(gid, indent+1)

	// --- 返回将在函数退出时执行的闭包 ---
	return func() {
		// --- 函数退出逻辑 ---
		// 加载当前 goroutine 的缩进级别。注意：可能已被并发调用修改。
		currentRawIndent, loaded := goroutineIndent.Load(gid)
		// 理论上 'loaded' 应始终为 true，因为进入时至少存储了 0。
		if !loaded {
			fmt.Fprintf(os.Stderr, "警告: goroutine %d 的缩进信息在退出时意外丢失 (函数: %s)\n", gid, name)
			// 即使丢失，也尝试打印退出日志，使用进入时的 indent 以保持对称
			printTrace(gid, name, line, "<-", int(indent))
			return
		}

		currentIndent := currentRawIndent.(int64)

		// 打印函数退出的追踪信息。
		// 关键：使用 *进入时* 的缩进级别 `indent` 来打印，使得进入和退出日志视觉对齐。
		printTrace(gid, name, line, "<-", int(indent))

		// 函数执行完毕，减少此 goroutine 的缩进级别。
		newExitIndent := currentIndent - 1
		if newExitIndent <= 0 {
			// 如果缩进级别降至 0 或更低，从 map 中移除此 goroutine 的条目。
			// 这有助于防止潜在的 goroutine ID 重用问题，并能释放少量内存。
			goroutineIndent.Delete(gid)
		} else {
			// 否则，更新缩进级别为递减后的值。
			goroutineIndent.Store(gid, newExitIndent)
		}
	}
}

// ============================================================================
// 2. AST 插桩逻辑 (修改 Go 代码结构)
// ============================================================================

// instrumenter 结构体封装了 AST 修改的相关逻辑。
// 在单文件版本中，它不需要存储配置状态。
type instrumenter struct{}

// NewInstrumenter 创建一个新的 instrumenter 实例。
func NewInstrumenter() *instrumenter {
	// 由于没有状态，直接返回零值结构体指针即可。
	return &instrumenter{}
}

// Instrument 是核心的插桩处理函数。它接收一个 Go 源文件的路径，
// 解析文件内容为 AST，遍历 AST 寻找函数声明，并在符合条件的函数体
// 开头插入 `defer Trace()()` 语句，最后将修改后的 AST 格式化回源代码。
// filename: 要处理的 Go 源文件的完整路径。
// 返回值:
//   - []byte: 修改并格式化后的源代码字节切片。
//   - error: 处理过程中发生的错误（如文件读取、解析、格式化失败）。
//
// 如果文件无需修改（例如没有函数或所有函数已插桩），则返回 (nil, nil)。
func (a *instrumenter) Instrument(filename string) ([]byte, error) {
	// 1. 解析源文件
	fset := token.NewFileSet() // 创建文件集，用于管理源码位置信息（对报错和格式化很重要）
	// 解析文件，保留注释信息 (parser.ParseComments)
	// parser.ParseFile 会读取文件内容并构建 AST
	curAST, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		// 文件本身存在语法错误，无法解析
		return nil, fmt.Errorf("解析文件 %s 时出错: %w", filename, err)
	}

	// 2. 遍历和修改 AST
	var modified bool // 标记是否对 AST 进行了实际修改

	// 使用 go/ast 包提供的 Apply 函数遍历 AST 节点 (Go 1.20+)
	// (对于 < Go 1.20，请使用 golang.org/x/tools/go/ast/astutil.Apply)
	// Apply 接受一个 AST 根节点和两个访问者函数：pre (访问前) 和 post (访问后)
	ast.Inspect(curAST, func(n ast.Node) bool { // Go 1.20+ 使用 ast.Inspect 更简洁
		// 检查当前节点是否为函数声明 (*ast.FuncDecl)
		fd, ok := n.(*ast.FuncDecl)
		// 如果不是函数声明，或者函数没有函数体 (例如接口方法、外部函数声明)，
		// 则不进行处理，继续遍历子节点。
		if !ok || fd.Body == nil {
			return true // 返回 true 表示继续遍历此节点的子节点
		}

		// 尝试向函数体添加 defer 语句
		if a.addDeferStmt(fd) {
			modified = true // 如果成功添加，标记为已修改
		}
		return true // 继续遍历
	})
	/* // Go < 1.20 使用 astutil.Apply 的示例:
	astutil.Apply(curAST, func(c *astutil.Cursor) bool {
		fd, ok := c.Node().(*ast.FuncDecl)
		if !ok || fd.Body == nil {
			return true
		}
		if a.addDeferStmt(fd) {
			modified = true
		}
		return true
	}, nil) // post 函数为 nil
	*/

	// 3. 如果遍历后未进行任何修改，则直接返回 nil, nil
	if !modified {
		return nil, nil
	}

	// 4. 格式化修改后的 AST
	// 在单文件版本中，Trace 函数就在 main 包内，我们不需要（也不能）添加 import 语句。
	var buf bytes.Buffer // 创建一个缓冲区来高效地接收格式化后的代码
	// 使用 go/format 标准库将修改后的 AST 节点（整个文件 curAST）格式化回 Go 代码
	err = format.Node(&buf, fset, curAST)
	if err != nil {
		// 格式化通常是健壮的，除非 AST 结构在修改过程中被严重破坏
		return nil, fmt.Errorf("格式化 %s 的新代码时出错: %w", filename, err)
	}

	// 5. 返回格式化后的代码字节切片
	return buf.Bytes(), nil
}

// addDeferStmt 尝试向给定的函数声明 (fd) 的函数体开头插入 "defer Trace()()" 语句。
// 此函数具有幂等性：如果目标 defer 语句已经存在，则不进行任何操作并返回 false。
// fd: 指向要修改的函数声明 (*ast.FuncDecl) 的指针。
// 返回值: bool - 如果成功添加了 defer 语句，则返回 true；否则（已存在或无法添加）返回 false。
func (a *instrumenter) addDeferStmt(fd *ast.FuncDecl) (added bool) {
	// 前提条件检查：函数必须有函数体才能添加语句。
	if fd.Body == nil {
		return false
	}
	// 如果函数体原本是空的 (例如 `func foo() {}`)，其语句列表 fd.Body.List 可能为 nil。
	// 我们需要确保它是一个（可能为空的）切片，以便后续追加。
	if fd.Body.List == nil {
		fd.Body.List = make([]ast.Stmt, 0, 1) // 初始化为一个容量为 1 的空切片
	}
	stmts := fd.Body.List // 获取（可能刚初始化的）语句列表

	// --- 幂等性检查：防止重复注入 ---
	// 遍历函数体现有的语句
	for _, stmt := range stmts {
		// 检查是否为 defer 语句
		ds, ok := stmt.(*ast.DeferStmt)
		if !ok {
			continue // 不是 defer，检查下一条
		}
		// 检查 defer 调用的是否为 "函数调用结果" 的形式，即 f()()
		outerCall, ok := ds.Call.Fun.(*ast.CallExpr)
		if !ok {
			// 不是 defer f()() 的形式，例如 defer f(a)，不匹配
			continue
		}
		// 检查内部被调用的函数是否为一个简单的标识符 (Identifier)
		ident, ok := outerCall.Fun.(*ast.Ident)
		if !ok {
			// 内部调用的不是标识符，例如 defer pkg.Func()()，不匹配
			continue
		}
		// 检查这个标识符的名字是否是我们追踪函数的名字 "Trace"
		if ident.Name == traceFuncName {
			// 找到了完全匹配的 "defer Trace()()" 语句
			return false // 已经存在，无需添加，返回 false
		}
	}

	// --- 如果代码执行到这里，说明需要添加 defer 语句 ---

	// 构造新的 defer 语句对应的 AST 节点: defer Trace()()
	newDeferStmt := &ast.DeferStmt{
		// DeferStmt 的 Call 字段表示 defer 关键字后面的表达式
		Call: &ast.CallExpr{ // 最外层的 ()
			// Fun 字段是被调用的函数，这里是 Trace() 的调用结果
			Fun: &ast.CallExpr{ // 内层的 ()
				// Fun 字段是被调用的函数，这里是标识符 Trace
				Fun: &ast.Ident{
					Name: traceFuncName, // "Trace"
				},
				// Args: 内层调用的参数列表，为空 []ast.Expr{}
				// Lparen, Rparen: 调用括号的位置信息，format.Node 会自动处理
			},
			// Args: 外层调用的参数列表，为空 []ast.Expr{}
		},
		// Defer: defer 关键字的位置信息，format.Node 会自动处理
	}

	// 将新构造的 defer 语句插入到现有语句列表的开头
	// 创建一个足够容纳所有旧语句 + 1 个新语句的新切片
	newList := make([]ast.Stmt, 0, len(stmts)+1)
	// 使用 append 将新语句放在最前面
	newList = append(newList, newDeferStmt)
	// 再将所有旧语句追加到后面
	newList = append(newList, stmts...)
	// 用新的列表替换函数体的旧列表
	fd.Body.List = newList

	return true // 成功添加了 defer 语句，返回 true
}

// ============================================================================
// 3. 命令行处理逻辑 (解析参数、遍历文件、调用插桩)
// ============================================================================

var (
	// write 标志决定是否将修改写回源文件。
	// 使用 flag.Bool 定义一个名为 "w" 的布尔标志，默认值为 false。
	// 变量 write 将存储该标志的值。
	write = flag.Bool("w", false, "将结果写入源文件而不是标准输出")
)

// usage 函数定义了当用户提供无效参数或请求帮助时，如何打印用法信息。
// 它会被赋值给 flag.Usage。
func usage() {
	// 将用法信息打印到标准错误输出 (stderr)
	fmt.Fprintf(os.Stderr, "用法: go run instrument.go [-w] [path]\n")
	fmt.Fprintf(os.Stderr, "       instrument [-w] [path] (编译后)\n\n") // 编译后的用法
	fmt.Fprintf(os.Stderr, "  path: 要处理的 Go 文件或目录。\n")
	fmt.Fprintf(os.Stderr, "        如果省略，默认为当前目录。\n")
	fmt.Fprintf(os.Stderr, "        递归处理目录下的 .go 文件 (自动跳过 _test.go, vendor/, .* 目录)。\n")
	fmt.Fprintf(os.Stderr, "选项:\n")
	flag.PrintDefaults() // 打印所有已定义的命令行标志的默认值和帮助信息
}

// main 函数是整个程序的入口点。
func main() {
	flag.Usage = usage // 将自定义的 usage 函数设置为 flag 包的用法处理函数
	flag.Parse()       // 解析命令行参数，填充由 flag.Bool 等定义的变量 (如此处的 write)

	// --- 获取目标路径 ---
	args := flag.Args() // 获取命令行参数中非标志的部分 (例如，文件或目录路径)
	targetPath := "."   // 如果用户未提供路径，默认处理当前目录 "."
	if len(args) > 0 {
		targetPath = args[0] // 使用用户提供的第一个非标志参数作为目标路径
	}

	// --- 路径验证和规范化 ---
	// 获取目标路径的绝对路径，这有助于后续一致地处理和比较路径
	absTargetPath, err := filepath.Abs(targetPath)
	if err != nil {
		// 如果获取绝对路径失败（通常不太可能发生）
		fmt.Fprintf(os.Stderr, "错误: 获取绝对路径 '%s' 失败: %v\n", targetPath, err)
		os.Exit(1) // 退出程序
	}

	// 检查目标路径是否存在以及它是一个文件还是目录
	targetInfo, err := os.Stat(absTargetPath)
	if err != nil {
		if os.IsNotExist(err) { // 如果路径不存在
			fmt.Fprintf(os.Stderr, "错误: 路径 '%s' 不存在\n", absTargetPath)
		} else { // 其他访问错误（例如权限问题）
			fmt.Fprintf(os.Stderr, "错误: 访问路径 '%s' 时出错: %v\n", absTargetPath, err)
		}
		os.Exit(1) // 路径有问题，无法继续，退出程序
	}

	// --- 初始化插桩器 ---
	ins := NewInstrumenter() // 创建 instrumenter 实例

	// --- 获取工具自身路径，用于避免处理自身 ---
	selfPath := "" // 用于存储编译后的可执行文件路径
	// os.Executable() 返回启动当前进程的可执行文件的路径名
	if execPath, execErr := os.Executable(); execErr == nil {
		selfPath, _ = filepath.Abs(execPath) // 获取绝对路径
	}
	runFilePath := "" // 用于存储通过 'go run' 启动时的源文件路径
	// runtime.Caller(0) 获取当前函数的调用者信息（即 main 函数），包括源文件路径
	_, runFile, _, runOk := runtime.Caller(0)
	if runOk {
		runFilePath, _ = filepath.Abs(runFile) // 获取绝对路径
	}
	// --- 工具路径获取结束 ---

	// --- 初始化处理结果统计 ---
	stats := struct {
		processed int // 已尝试处理的文件数 (符合 .go 后缀且非测试/自身)
		modified  int // 成功修改（并可能写入）的文件数
		skipped   int // 因规则（非Go、测试、vendor、隐藏、自身）而被跳过的文件/目录数
		errors    int // 处理过程中发生错误的文件数
	}{}

	// --- 主要处理逻辑：区分目录和文件 ---
	if targetInfo.IsDir() {
		// --- 情况 1: 目标是目录，需要递归遍历 ---
		fmt.Printf("递归处理目录: %s\n", absTargetPath)
		// 使用 filepath.WalkDir (Go 1.16+) 进行高效、安全的目录遍历
		err := filepath.WalkDir(absTargetPath, func(path string, d fs.DirEntry, walkErr error) error {
			// 1. 处理 WalkDir 本身可能遇到的错误（例如，进入目录时的权限问题）
			if walkErr != nil {
				fmt.Fprintf(os.Stderr, "警告: 访问路径 '%s' 出错: %v, 跳过\n", path, walkErr)
				stats.errors++ // 记录错误
				// 可以选择返回 filepath.SkipDir 跳过整个有问题的子目录，但这里选择仅报告错误并继续
				return nil // 返回 nil 继续处理其他文件/目录
			}

			// 获取当前遍历项的绝对路径，用于与自身路径比较
			absPath, pathErr := filepath.Abs(path)
			if pathErr != nil {
				fmt.Fprintf(os.Stderr, "警告: 获取绝对路径 '%s' 失败: %v, 跳过\n", path, pathErr)
				stats.errors++
				return nil // 跳过此项
			}

			// 2. 跳过特定目录：vendor, 隐藏目录 (名称以 "." 开头)
			if d.IsDir() { // 检查是否是目录
				dirName := d.Name()
				// 跳过 vendor 目录，以及名称以 "." 开头的目录 (如 .git, .idea, .vscode)
				// 同时排除 "." 和 ".." 这两个特殊目录本身
				if dirName == "vendor" || (dirName != "." && dirName != ".." && strings.HasPrefix(dirName, ".")) {
					fmt.Printf("跳过目录: %s\n", path)
					stats.skipped++         // 增加跳过计数
					return filepath.SkipDir // 指示 WalkDir 不要进入此目录及其子目录
				}
				return nil // 是需要处理的目录，继续深入遍历
			}

			// 3. 只处理普通文件（跳过符号链接、设备文件等）
			if !d.Type().IsRegular() {
				stats.skipped++
				return nil
			}

			// 4. 文件过滤：只处理 .go 文件，并且不是 _test.go 文件
			fileName := d.Name()
			if !strings.HasSuffix(fileName, ".go") || strings.HasSuffix(fileName, "_test.go") {
				if strings.HasSuffix(fileName, ".go") { // 是 _test.go 文件
					fmt.Printf("跳过测试文件: %s\n", path)
					stats.skipped++
				} else { // 其他非 Go 文件
					// 可以选择性地跳过这些文件的日志输出，以保持简洁
					// stats.skipped++
				}
				return nil // 跳过此文件
			}

			// 5. 跳过工具自身文件
			if absPath == selfPath || absPath == runFilePath {
				fmt.Printf("跳过工具自身文件: %s\n", path)
				stats.skipped++
				return nil
			}

			// --- 文件符合所有条件，执行插桩处理 ---
			fmt.Printf("处理文件: %s\n", path)
			stats.processed++ // 增加已处理文件计数
			// 调用核心处理函数
			modified, procErr := processFile(path, ins, *write)
			if procErr != nil { // 处理过程中发生错误
				fmt.Fprintf(os.Stderr, "  错误: 处理 %s 失败: %v\n", path, procErr)
				stats.errors++ // 增加错误计数
			} else if modified { // 处理成功且文件被修改
				writeMsg := ""
				if *write {
					writeMsg = "并写入"
				} // 根据 -w 标志添加提示
				fmt.Printf("  成功: 文件 %s 已插桩%s。\n", path, writeMsg)
				stats.modified++ // 增加修改计数
			} else { // 处理成功但文件无需修改
				fmt.Printf("  信息: 文件 %s 无需修改。\n", path)
			}

			return nil // 继续遍历下一个文件/目录
		})
		// 检查 WalkDir 本身是否返回了错误（非 walkFn 中处理的错误）
		if err != nil {
			fmt.Fprintf(os.Stderr, "错误: 遍历目录 '%s' 时发生意外错误: %v\n", absTargetPath, err)
			stats.errors++
		}
	} else {
		// --- 情况 2: 目标是单个文件 ---
		fileName := targetInfo.Name()
		absPath := absTargetPath // 对于单个文件，目标绝对路径就是文件的绝对路径

		// 执行与目录遍历中类似的检查
		// 1. 检查是否是有效的 Go 源文件 (非测试文件)
		if !strings.HasSuffix(fileName, ".go") || strings.HasSuffix(fileName, "_test.go") {
			if strings.HasSuffix(fileName, "_test.go") { // 是测试文件
				fmt.Printf("跳过测试文件: %s\n", absPath)
				stats.skipped++
			} else { // 不是 Go 文件
				fmt.Fprintf(os.Stderr, "错误: '%s' 不是有效的 Go 源文件。\n", absPath)
				stats.errors++
			}
		} else if absPath == selfPath || absPath == runFilePath { // 2. 检查是否是工具自身
			fmt.Printf("跳过工具自身文件: %s\n", absPath)
			stats.skipped++
		} else {
			// --- 文件符合条件，执行插桩处理 ---
			fmt.Printf("处理单个文件: %s\n", absPath)
			stats.processed++                                      // 增加处理计数
			modified, procErr := processFile(absPath, ins, *write) // 调用核心处理函数
			if procErr != nil {                                    // 处理出错
				fmt.Fprintf(os.Stderr, "  错误: 处理 %s 失败: %v\n", absPath, procErr)
				stats.errors++ // 增加错误计数
			} else if modified { // 处理成功且被修改
				writeMsg := ""
				if *write {
					writeMsg = "并写入"
				}
				fmt.Printf("  成功: 文件 %s 已插桩%s。\n", absPath, writeMsg)
				stats.modified++ // 增加修改计数
			} else { // 处理成功但无需修改
				fmt.Printf("  信息: 文件 %s 无需修改。\n", absPath)
			}
		}
	}

	// --- 打印处理结果总结 ---
	fmt.Println("\n--- 处理完成 ---")
	fmt.Printf("已处理 Go 文件数: %d\n", stats.processed)
	fmt.Printf("成功修改文件数:  %d\n", stats.modified)
	fmt.Printf("跳过文件/目录数: %d\n", stats.skipped)
	fmt.Printf("处理失败文件数:  %d\n", stats.errors)

	// --- 程序退出 ---
	// 如果在处理过程中记录了任何错误，程序以非零状态码退出，
	// 这可以被脚本或其他自动化工具捕获。
	if stats.errors > 0 {
		os.Exit(1)
	}
	// 如果没有错误，程序以状态码 0 正常退出。
}

// processFile 函数负责处理单个文件的插桩操作，包括可选的文件写入。
// path: 要处理的文件的绝对路径。
// ins: instrumenter 实例，用于执行 AST 修改。
// writeToFile: 布尔值，指示是否应将修改后的内容写回文件。
// 返回值:
//   - bool: 表示文件是否被成功修改（即使未写入）。
//   - error: 处理过程中发生的错误（例如，插桩分析失败或文件写入失败）。
func processFile(path string, ins *instrumenter, writeToFile bool) (bool, error) {
	// 1. 调用 Instrumenter 的核心方法进行代码分析和修改
	// Instrument 返回修改后的代码（如果需要修改）或 nil（如果不需要修改）
	newSrc, err := ins.Instrument(path)
	if err != nil {
		// 如果 Instrument 内部发生错误（例如，源文件有语法错误导致解析失败）
		// 将错误包装一下，提供更清晰的上下文
		return false, fmt.Errorf("插桩分析失败: %w", err)
	}

	// 2. 检查是否需要修改
	if newSrc == nil {
		// Instrument 返回 nil 表示代码无需修改
		return false, nil // 返回 false 表示未修改，error 为 nil 表示处理成功
	}

	// --- 代码已被修改 (newSrc != nil) ---
	modified := true // 标记为已修改

	// 3. 处理输出：写入文件或打印提示
	if !writeToFile {
		// 如果不需要写回文件 (-w 未指定)
		// 打印一条提示信息，而不是将可能非常长的代码打印到控制台
		fmt.Println("  (检测到修改，但未写入文件。使用 -w 选项写入)")
	} else {
		// 需要将修改后的代码写回源文件
		// a. 获取原始文件信息，尝试保留其权限位
		originalInfo, statErr := os.Stat(path)
		perms := fs.FileMode(0644) // 设置一个安全的默认文件权限 (所有者读写，组和其他用户只读)
		if statErr == nil {
			perms = originalInfo.Mode().Perm() // 如果成功获取，则使用原始文件的权限位
		} else {
			// 如果获取原始权限失败，打印警告，但继续使用默认权限
			fmt.Fprintf(os.Stderr, "警告: 无法获取文件 %s 的原始权限: %v, 将使用默认权限 0644\n", path, statErr)
		}

		// b. 使用 os.WriteFile 将新内容写入文件，覆盖原有内容
		// WriteFile 会原子性地（或尽可能原子性地）写入文件，并处理文件关闭
		err = os.WriteFile(path, newSrc, perms)
		if err != nil {
			// 如果写入文件时发生 IO 错误（例如权限不足、磁盘空间满）
			// 返回错误，同时 modified 仍然是 true，因为插桩分析是成功的
			return modified, fmt.Errorf("写入文件 '%s' 失败: %w", path, err)
		}
	}

	// 如果执行到这里，表示修改成功（无论是否写入）且没有发生错误
	return modified, nil
}
